(function() {
	/* constants */
	var LBUTTON = 0, MBUTTON = 1, RBUTTON = 2;
	var LEFT = 'L', RIGHT = 'R', UP = 'U', DOWN = 'D';

	/* mouse pointer's last cordinates and distances moved */
	var _lastX, _lastY, _accuX, _accuY;

	/* threshold for leaned (like "left up") strokes
	 * this feature is on hold because the matching rate is low */
	var _lean = 1 / Math.tan(30 * Math.PI / 180);

	/* the stroking path object, will be passed to the stroke handler */
	var _path;

	/* the stroke movement's end time */
	var _endtm;

	/* true if the last stroke is fired */
	var _fired;

	/* true if this script is embeded in the extension's option page */
	var _optpg = chrome.extension.getURL('options.html') === location.href;

	/* the <canvas> element and its context for trail drawing */
	var _tcanvas, _tcontex;

	/* the connection port between the backend page */
	var _port = chrome.extension.connect({name: 'stroke.html-stroke'});

	/* configurations */
	var _cfg = {
		/* default values */
		trigger: RBUTTON,
		timeout: 1000,
		minstep: 20,
		trail: '',
		suppress: true
	}

	/* check if a stroke routine should be executed */
	var check = function(ev) {
		return ev.button === _cfg.trigger &&
			(!_cfg.suppress || !ev.altKey && !ev.metaKey);
	}

	/* calculate weights by distances moved in both directions */
	var weigh = function(x, y) {
		return Math.round(Math.sqrt(x * x + y * y));
	}

	/* search for image that is possibly positioned at the given
	 * element's coordinate */
	var imgsrc = function(elem) {
		var src;
		if (!elem)
			return null;
		else if ('IMG' == elem.nodeName && !!elem.src)
			return elem.src;
		else if (src = getComputedStyle(elem).getPropertyValue('background-image'))
			/* getPropertyValue() returns string literal 'none' if
			 * no background images are set */
			return src.match(/^url\(.+\)$/) && src.replace(/^url\((.+)\)$/, '$1');
		else return imgsrc(elem.parentNode);
	}

	/* strip MouseEvent object to void 'Converting circular structure to
	 * JSON' errors when passing them around */
	var strip = function(ev) {
		/* keep useful properties only, also retain the MouseEvent
		 * object structure */
		return {
			srcElement: {
				/* link type strokes need these properties */
				textContent: ev.srcElement.textContent,
				href: ev.srcElement.href,
				/* image type strokes need these */
				src: imgsrc(ev.srcElement),
				/* they all got this */
				nodeName: ev.srcElement.nodeName
			}
		};
	}

	/* create the canvas element for trail drawing */
	var canvas = function() {
		_tcanvas = document.createElement('canvas');
		_tcanvas.width = window.innerWidth;
		_tcanvas.height = window.innerHeight;
		_tcanvas.style.cssText = 'left:0;position:fixed;top:0;z-index:' + new Date().getTime() + ';';
		_tcontex = _tcanvas.getContext('2d');
		/* styling */
		_tcontex.globalAlpha = 0.3;
		_tcontex.lineWidth = 3;
		_tcontex.lineCap = 'round';
		_tcontex.shadowOffsetX = 0;
		_tcontex.shadowOffsetY = 0;
		_tcontex.shadowBlur = 3;
		_tcontex.shadowColor = _cfg.trail;
		_tcontex.strokeStyle = _cfg.trail;
	}

	/* initiate a stroke routine on mouse button down */
	window.addEventListener('mousedown', function(ev) {
		if (!check(ev)) return;

		if (_cfg.trail) {
			/* prepare to draw trails */
			if (!_tcontex) canvas();
			_tcontex.beginPath();
			_tcontex.moveTo(ev.clientX, ev.clientY);
		}

		/* start recording path, reset everything */
		_lastX = ev.screenX;
		_lastY = ev.screenY;
		_accuX = _accuY = 0;
		_fired = false;
		_path = {
			/* stroke array */
			stroke: [],
			/* weights of each stroke part in the stroke array */
			weights: [],
			/* the mouse events are sent to the stroke handler */
			mousedown: strip(ev),
			mouseup: null,
			/* currently selected text */
			seltext: window.getSelection().toString(),
			/* is this a user defined stroke? */
			custom: ev.ctrlKey
		};
	}, true);

	/* track mouse movements */
	window.addEventListener('mousemove', function(ev) {
		if (!check(ev) || !_path) return;
		var move, last_idx,
			x = ev.screenX,
			y = ev.screenY,
			offsetX = x - _lastX,
			offsetY = y - _lastY,
			absX = Math.abs(offsetX),
			absY = Math.abs(offsetY);

		/* the movement is negligible */
		if (absX < _cfg.minstep && absY < _cfg.minstep) return;

		if (_cfg.trail) {
			/* BUG=11
			 * insert the canvas into the DOM tree now, other
			 * than on the moment when it was created, because
			 * operations on <body> will cause any selected
			 * elements to lose focus */
			if (!_tcanvas.appended) {
				document.body.insertBefore(_tcanvas,
					document.body.firstChild);
				_tcanvas.appended = true;
			}
			_tcontex.lineTo(ev.clientX, ev.clientY);
			_tcontex.stroke();
		}
		_lastX = x;
		_lastY = y;
		_accuX += absX;
		_accuY += absY;

		/* ignore leaning, blurrer strokes are more recognizable */
//		if (0 == absY || (absX / absY > _lean)) {
//		} else if (0 == absX || (absY / absX > _lean)) {
//		} else {
//			move = (offsetX > 0 ? RIGHT : LEFT)
//				+ (offsetY > 0 ? DOWN : UP);
//		}
		if (absX > absY) {
			move = offsetX > 0 ? RIGHT : LEFT;
		} else {
			move = offsetY > 0 ? DOWN : UP;
		}

		last_idx = _path.stroke.length - 1;
		if (-1 == last_idx || _path.stroke[last_idx] != move) {
			/* first move or direction changed */
			_accuX = absX;
			_accuY = absY;
			_path.stroke.push(move);
			_path.weights.push(weigh(_accuX, _accuY));
		} else {
			/* update weights */
			_path.weights[last_idx] = weigh(_accuX, _accuY);
		}
		/* update movement's ending time */
		_endtm = new Date().getTime();
	}, true);

	/* finish stroking */
	window.addEventListener('mouseup', function(ev) {
		if (!check(ev)) {
			/* when mousemove event is triggered, the event.button
			 * is 0 either if the left button is pressed or not,
			 * so there is no way to tell if the left button is
			 * pressed.
			 * undefine _path here so that the mousemove event
			 * listener knows it's not in a stroke transaction
			 * when left button is configured as stroke trigger.
			 * http://www.quirksmode.org/js/events_properties.html */
			_path = undefined;
			return;
		}

		/* trail should be cleared even if it's not a valid stroke */
		if (_cfg.trail) {
			_tcontex.clearRect(0, 0, _tcanvas.width, _tcanvas.height);
			if (_tcanvas.appended) {
				document.body.removeChild(_tcanvas);
				_tcanvas.appended = false;
			}
		}

		if (!_path || !_path.stroke.length ||
			(new Date().getTime() - _endtm) > _cfg.timeout) {
			/* no strokes or timeouted */
			_path = undefined;
			return;
		}

		/* it's a valid stroke */
		_fired = true;
		_path.mouseup = strip(ev);

		if (_optpg) {
			/* post event for options.html */
			var e = document.createEvent('Event');
			e.initEvent('set-stroke-event', false, false);
			e.path = _path;
			document.dispatchEvent(e);
		} else {
			/* post regular strokes */
			_port.postMessage(_path);
		}
		_path = undefined;
	}, true);

	/* suppress the context menu when the trigger is right mouse button */
	window.addEventListener('contextmenu', function(ev) {
		if (_fired && _cfg.trigger == RBUTTON)
			ev.preventDefault();
	}, true);

	/* response to requests from background page */
	chrome.extension.onRequest.addListener(function(req, sender, resp) {
		if ('update-config' == req.action) {
			_cfg = req.data;
		} else if ('local-eval' == req.action) {
			var ex, id = req.data.id, tab = req.data.tab,
				path = req.data.path,
				custom = req.data.custom,
				script = req.data.script;

			try {
				if (custom) {
					/* path is passed as the magic _env
					 * variable to the sandbox */
					ex = sandbox.call(tab, path, script);
					if (true !== ex) throw ex;
				} else {
					_actions[id].call(tab, tab, path);
				}
			} catch (ex) {
				console.error(ex);
			}
		}
		try { resp({}) } catch (e) {}
	});

	/* query config from the background page */
	_port.postMessage(null);
})();
